/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.client.caller;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * 複数のサーバーに対してタイムアウト指定付きでリモートプロシージャーコールを行うようにするための、{@link RPCClientCaller}のラッパークラス。<br>
 * <i>(Decoratorパターンを使用)</i><br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class TimeoutRPCClientCaller extends AbstractRPCClientCaller {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(TimeoutRPCClientCaller.class);


	//==========================================================
	//フィールド

	/**
	 * 複数のサーバーに対してリモートプロシージャーコールを行うインターフェース
	 */
	protected final RPCClientCaller caller;

	/**
	 * タイムアウト時間(単位=ミリ秒)
	 */
	public final int timeoutMilliseconds;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 *
	 * @param caller 複数のサーバーに対してリモートプロシージャーコールを行うインターフェース
	 * @param timeoutMilliseconds タイムアウト時間(単位=ミリ秒)
	 * @throws IllegalArgumentException タイムアウト時間に0以下の値が指定された場合
	 */
	public TimeoutRPCClientCaller(RPCClientCaller caller,
			int timeoutMilliseconds) throws IllegalArgumentException {
		//==========================================================
		//引数のチェック

		//タイムアウト時間が0以下の場合
		if (timeoutMilliseconds <= 0) {
			//不正な引数の場合の例外を投げる
			throw new IllegalArgumentException("タイムアウト時間に0以下の値を指定することはできません。");
		}


		//==========================================================
		//引数の値をフィールドに設定

		//複数のサーバーに対してリモートプロシージャーコールを行うインターフェース
		this.caller = caller;

		//タイムアウト時間(単位=ミリ秒)
		this.timeoutMilliseconds = timeoutMilliseconds;
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object remoteCall(final URLListGetter urlListGetter,
			final String methodName, final Object... args) throws Exception {
		//単一スレッド用のExecutorServiceを生成
		ExecutorService ex = Executors.newSingleThreadExecutor();

		try {
			//値を返す実行タスクを送信し、保留中のタスク結果を表すFutureを取得する
			//(タスクの内容は単純なので、Callableインターフェースを無名インナークラスで実装)
			Future<Object> future = ex.submit(new Callable<Object>() {
				@Override
				public Object call() throws Exception {
					//処理に成功するか割り込みが発生するまで繰り返したいので、無限ループをまわす
					while (true) {
						//現在のスレッドが割り込まれている場合
						//(おそらくタイムアウトになった場合)
						if (Thread.currentThread().isInterrupted()) {
							//デバッグログが有効な場合
							if (log.isDebugEnabled()) {
								//デバッグログ出力
								log.debug("複数のサーバーに対してタイムアウト指定付きでリモートプロシージャーコールを行っている途中で、リモートプロシージャーコールの途中で割り込みを検知しました。");
							}

							//割り込みが発生した場合の例外を投げる
							throw new InterruptedException("複数のサーバーに対してタイムアウト指定付きでリモートプロシージャーコールを行っている途中で、割り込みが発生しました。");
						}

						//複数のサーバーに対してリモートプロシージャーコールを行うインターフェースに処理を委譲する
						return TimeoutRPCClientCaller.this.caller.remoteCall(urlListGetter, methodName, args);
					}
				}
			});

			try {
				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("*** タイムアウト時間:" + this.timeoutMilliseconds
							+ " ミリ秒の設定で、" + methodName
							+ "()メソッドに対するリモートプロシージャコールを行います ***");
				}

				//タイムアウト指定付きでFutureからタスクの結果を取得して、そのまま帰す
				return future.get(this.timeoutMilliseconds, TimeUnit.MILLISECONDS);
			}
			//処理がタイムアウトした場合
			catch (TimeoutException te) {
				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("*** リモートプロシージャコールがタイムアウトしました ***");
				}

				//捕捉した例外をそのまま投げる
				throw te;
			}
			//タスクの実行中に例外が発生した場合
			catch (ExecutionException ee) {
				//例外の発生元が取得できて、かつExceptionのサブクラスの場合
				if ((ee.getCause() != null)
						&& (ee.getCause() instanceof Exception)) {
					//例外の発生元をExceptionにキャストして返す
					throw (Exception) ee.getCause();
				}
				//それ以外の場合
				else {
					//捕捉した例外をそのまま投げる
					throw ee;
				}
			}
		}
		//終了処理
		finally {
			//ExecutorServiceをシャットダウンして、以降のタスクが実行されないようにする
			ex.shutdownNow();
		}
	}
}
